package org.eclipse.che.maven;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import org.apache.maven.RepositoryUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.metadata.ArtifactMetadataRetrievalException;
import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
import org.apache.maven.artifact.metadata.ResolutionGroup;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.repository.LegacyLocalRepositoryManager;
import org.apache.maven.artifact.repository.RepositoryRequest;
import org.apache.maven.artifact.repository.metadata.Snapshot;
import org.apache.maven.artifact.repository.metadata.SnapshotArtifactRepositoryMetadata;
import org.apache.maven.artifact.resolver.AbstractArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactCollector;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.artifact.resolver.DebugResolutionListener;
import org.apache.maven.artifact.resolver.ResolutionErrorHandler;
import org.apache.maven.artifact.resolver.ResolutionListener;
import org.apache.maven.artifact.resolver.ResolutionNode;
import org.apache.maven.artifact.resolver.WarningResolutionListener;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.LegacySupport;
import org.apache.maven.repository.legacy.metadata.DefaultMetadataResolutionRequest;
import org.apache.maven.repository.legacy.metadata.MetadataResolutionRequest;
import org.apache.maven.repository.legacy.resolver.conflict.ConflictResolver;
import org.apache.maven.wagon.events.TransferListener;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.logging.Logger;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.Disposable;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.LocalRepositoryManager;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.che.maven.data.MavenKey;
import org.eclipse.che.maven.data.MavenWorkspaceCache;
import org.eclipse.che.maven.data.MavenWorkspaceCache.Entry;

/**
 * Copied from org.apache.maven.artifact.resolver.DefaultArtifactResolver changed method {@link
 * CheArtifactResolver#resolve(Artifact, List, RepositorySystemSession)}, it's avoid resolving
 * artifacts provided by maven workspace.
 *
 * @author Evgen Vidolob
 */
@Component(role = ArtifactResolver.class, hint = "che")
public class CheArtifactResolver implements ArtifactResolver, Disposable {
  @Requirement private Logger logger;

  @Requirement protected ArtifactFactory artifactFactory;

  @Requirement private ArtifactCollector artifactCollector;

  @Requirement private ResolutionErrorHandler resolutionErrorHandler;

  @Requirement private ArtifactMetadataSource source;

  @Requirement private PlexusContainer container;

  @Requirement private LegacySupport legacySupport;

  @Requirement private RepositorySystem repoSystem;

  private final Executor executor;

  private volatile MavenWorkspaceCache workspaceCache;
  private boolean failOnUnresolvedDependency;

  public CheArtifactResolver() {
    int threads = Integer.getInteger("maven.artifact.threads", 5);
    if (threads <= 1) {
      executor =
          new Executor() {
            public void execute(Runnable command) {
              command.run();
            }
          };
    } else {
      executor =
          new ThreadPoolExecutor(
              threads,
              threads,
              3,
              TimeUnit.SECONDS,
              new LinkedBlockingQueue<Runnable>(),
              new DaemonThreadCreator());
    }
  }

  private RepositorySystemSession getSession(ArtifactRepository localRepository) {
    return LegacyLocalRepositoryManager.overlay(
        localRepository, legacySupport.getRepositorySession(), repoSystem);
  }

  private void injectSession1(RepositoryRequest request, MavenSession session) {
    if (session != null) {
      request.setOffline(session.isOffline());
      request.setForceUpdate(session.getRequest().isUpdateSnapshots());
    }
  }

  private void injectSession2(ArtifactResolutionRequest request, MavenSession session) {
    injectSession1(request, session);

    if (session != null) {
      request.setServers(session.getRequest().getServers());
      request.setMirrors(session.getRequest().getMirrors());
      request.setProxies(session.getRequest().getProxies());
    }
  }

  public void resolve(
      Artifact artifact,
      List<ArtifactRepository> remoteRepositories,
      ArtifactRepository localRepository,
      TransferListener resolutionListener)
      throws ArtifactResolutionException, ArtifactNotFoundException {
    resolve(artifact, remoteRepositories, getSession(localRepository));
  }

  public void resolveAlways(
      Artifact artifact,
      List<ArtifactRepository> remoteRepositories,
      ArtifactRepository localRepository)
      throws ArtifactResolutionException, ArtifactNotFoundException {
    resolve(artifact, remoteRepositories, getSession(localRepository));
  }

  private void resolve(
      Artifact artifact,
      List<ArtifactRepository> remoteRepositories,
      RepositorySystemSession session)
      throws ArtifactResolutionException, ArtifactNotFoundException {
    if (isModule(artifact)) {
      return;
    }
    try {
      resolveOrig(artifact, remoteRepositories, session);
    } catch (AbstractArtifactResolutionException e) {
      if (!failOnUnresolvedDependency) {
        artifact.setResolved(true);
      }
    }
  }

  private boolean isModule(Artifact artifact) {
    MavenWorkspaceCache cache = workspaceCache;
    if (cache == null) {
      return false;
    }
    Entry entry =
        cache.findEntry(
            new MavenKey(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()));
    if (entry == null) {
      return false;
    }
    artifact.setResolved(true);
    artifact.setFile(entry.getFile(artifact.getType()));
    artifact.selectVersion(entry.getKey().getVersion());
    return true;
  }

  private void resolveOrig(
      Artifact artifact,
      List<ArtifactRepository> remoteRepositories,
      RepositorySystemSession session)
      throws ArtifactResolutionException, ArtifactNotFoundException {
    if (artifact == null) {
      return;
    }

    if (Artifact.SCOPE_SYSTEM.equals(artifact.getScope())) {
      File systemFile = artifact.getFile();

      if (systemFile == null) {
        throw new ArtifactNotFoundException(
            "System artifact: " + artifact + " has no file attached", artifact);
      }

      if (!systemFile.exists()) {
        throw new ArtifactNotFoundException(
            "System artifact: " + artifact + " not found in path: " + systemFile, artifact);
      }

      if (!systemFile.isFile()) {
        throw new ArtifactNotFoundException(
            "System artifact: " + artifact + " is not a file: " + systemFile, artifact);
      }

      artifact.setResolved(true);

      return;
    }

    if (!artifact.isResolved()) {
      ArtifactResult result;

      try {
        ArtifactRequest artifactRequest = new ArtifactRequest();
        artifactRequest.setArtifact(RepositoryUtils.toArtifact(artifact));
        artifactRequest.setRepositories(RepositoryUtils.toRepos(remoteRepositories));

        // Maven 2.x quirk: an artifact always points at the local repo, regardless whether resolved
        // or not
        LocalRepositoryManager lrm = session.getLocalRepositoryManager();
        String path = lrm.getPathForLocalArtifact(artifactRequest.getArtifact());
        artifact.setFile(new File(lrm.getRepository().getBasedir(), path));

        result = repoSystem.resolveArtifact(session, artifactRequest);
      } catch (org.eclipse.aether.resolution.ArtifactResolutionException e) {
        if (e.getCause() instanceof org.eclipse.aether.transfer.ArtifactNotFoundException) {
          throw new ArtifactNotFoundException(
              e.getMessage(),
              artifact.getGroupId(),
              artifact.getArtifactId(),
              artifact.getVersion(),
              artifact.getType(),
              artifact.getClassifier(),
              remoteRepositories,
              artifact.getDownloadUrl(),
              artifact.getDependencyTrail(),
              e);
        } else {
          throw new ArtifactResolutionException(e.getMessage(), artifact, remoteRepositories, e);
        }
      }

      artifact.selectVersion(result.getArtifact().getVersion());
      artifact.setFile(result.getArtifact().getFile());
      artifact.setResolved(true);

      if (artifact.isSnapshot()) {
        Matcher matcher = Artifact.VERSION_FILE_PATTERN.matcher(artifact.getVersion());
        if (matcher.matches()) {
          Snapshot snapshot = new Snapshot();
          snapshot.setTimestamp(matcher.group(2));
          try {
            snapshot.setBuildNumber(Integer.parseInt(matcher.group(3)));
            artifact.addMetadata(new SnapshotArtifactRepositoryMetadata(artifact, snapshot));
          } catch (NumberFormatException e) {
            logger.warn(
                "Invalid artifact version " + artifact.getVersion() + ": " + e.getMessage());
          }
        }
      }
    }
  }

  public ArtifactResolutionResult resolveTransitively(
      Set<Artifact> artifacts,
      Artifact originatingArtifact,
      ArtifactRepository localRepository,
      List<ArtifactRepository> remoteRepositories,
      ArtifactMetadataSource source,
      ArtifactFilter filter)
      throws ArtifactResolutionException, ArtifactNotFoundException {
    return resolveTransitively(
        artifacts,
        originatingArtifact,
        Collections.EMPTY_MAP,
        localRepository,
        remoteRepositories,
        source,
        filter);
  }

  public ArtifactResolutionResult resolveTransitively(
      Set<Artifact> artifacts,
      Artifact originatingArtifact,
      Map managedVersions,
      ArtifactRepository localRepository,
      List<ArtifactRepository> remoteRepositories,
      ArtifactMetadataSource source)
      throws ArtifactResolutionException, ArtifactNotFoundException {
    return resolveTransitively(
        artifacts,
        originatingArtifact,
        managedVersions,
        localRepository,
        remoteRepositories,
        source,
        null);
  }

  public ArtifactResolutionResult resolveTransitively(
      Set<Artifact> artifacts,
      Artifact originatingArtifact,
      Map managedVersions,
      ArtifactRepository localRepository,
      List<ArtifactRepository> remoteRepositories,
      ArtifactMetadataSource source,
      ArtifactFilter filter)
      throws ArtifactResolutionException, ArtifactNotFoundException {
    return resolveTransitively(
        artifacts,
        originatingArtifact,
        managedVersions,
        localRepository,
        remoteRepositories,
        source,
        filter,
        null);
  }

  public ArtifactResolutionResult resolveTransitively(
      Set<Artifact> artifacts,
      Artifact originatingArtifact,
      List<ArtifactRepository> remoteRepositories,
      ArtifactRepository localRepository,
      ArtifactMetadataSource source)
      throws ArtifactResolutionException, ArtifactNotFoundException {
    return resolveTransitively(
        artifacts, originatingArtifact, localRepository, remoteRepositories, source, null);
  }

  public ArtifactResolutionResult resolveTransitively(
      Set<Artifact> artifacts,
      Artifact originatingArtifact,
      List<ArtifactRepository> remoteRepositories,
      ArtifactRepository localRepository,
      ArtifactMetadataSource source,
      List<ResolutionListener> listeners)
      throws ArtifactResolutionException, ArtifactNotFoundException {
    return resolveTransitively(
        artifacts,
        originatingArtifact,
        Collections.EMPTY_MAP,
        localRepository,
        remoteRepositories,
        source,
        null,
        listeners);
  }

  public ArtifactResolutionResult resolveTransitively(
      Set<Artifact> artifacts,
      Artifact originatingArtifact,
      Map managedVersions,
      ArtifactRepository localRepository,
      List<ArtifactRepository> remoteRepositories,
      ArtifactMetadataSource source,
      ArtifactFilter filter,
      List<ResolutionListener> listeners)
      throws ArtifactResolutionException, ArtifactNotFoundException {
    return resolveTransitively(
        artifacts,
        originatingArtifact,
        managedVersions,
        localRepository,
        remoteRepositories,
        source,
        filter,
        listeners,
        null);
  }

  public ArtifactResolutionResult resolveTransitively(
      Set<Artifact> artifacts,
      Artifact originatingArtifact,
      Map managedVersions,
      ArtifactRepository localRepository,
      List<ArtifactRepository> remoteRepositories,
      ArtifactMetadataSource source,
      ArtifactFilter filter,
      List<ResolutionListener> listeners,
      List<ConflictResolver> conflictResolvers)
      throws ArtifactResolutionException, ArtifactNotFoundException {
    ArtifactResolutionRequest request =
        new ArtifactResolutionRequest()
            .setArtifact(originatingArtifact)
            .setResolveRoot(false)
            // This is required by the surefire plugin
            .setArtifactDependencies(artifacts)
            .setManagedVersionMap(managedVersions)
            .setLocalRepository(localRepository)
            .setRemoteRepositories(remoteRepositories)
            .setCollectionFilter(filter)
            .setListeners(listeners);

    injectSession2(request, legacySupport.getSession());

    return resolveWithExceptions(request);
  }

  public ArtifactResolutionResult resolveWithExceptions(ArtifactResolutionRequest request)
      throws ArtifactResolutionException, ArtifactNotFoundException {
    ArtifactResolutionResult result = resolve(request);

    // We have collected all the problems so let's mimic the way the old code worked and just blow
    // up right here.
    // That's right lets just let it rip right here and send a big incomprehensible blob of text at
    // unsuspecting
    // users. Bad dog!

    resolutionErrorHandler.throwErrors(request, result);

    return result;
  }

  // ------------------------------------------------------------------------
  //
  // ------------------------------------------------------------------------

  public ArtifactResolutionResult resolve(ArtifactResolutionRequest request) {
    Artifact rootArtifact = request.getArtifact();
    Set<Artifact> artifacts = request.getArtifactDependencies();
    Map<String, Artifact> managedVersions = request.getManagedVersionMap();
    List<ResolutionListener> listeners = request.getListeners();
    ArtifactFilter collectionFilter = request.getCollectionFilter();
    ArtifactFilter resolutionFilter = request.getResolutionFilter();
    RepositorySystemSession session = getSession(request.getLocalRepository());

    // TODO: hack because metadata isn't generated in m2e correctly and i want to run the maven i
    // have in the workspace
    if (source == null) {
      try {
        source = container.lookup(ArtifactMetadataSource.class);
      } catch (ComponentLookupException e) {
        // won't happen
      }
    }

    if (listeners == null) {
      listeners = new ArrayList<ResolutionListener>();

      if (logger.isDebugEnabled()) {
        listeners.add(new DebugResolutionListener(logger));
      }

      listeners.add(new WarningResolutionListener(logger));
    }

    ArtifactResolutionResult result = new ArtifactResolutionResult();

    // The root artifact may, or may not be resolved so we need to check before we attempt to
    // resolve.
    // This is often an artifact like a POM that is taken from disk and we already have hold of the
    // file reference. But this may be a Maven Plugin that we need to resolve from a remote
    // repository
    // as well as its dependencies.

    if (request.isResolveRoot() /* && rootArtifact.getFile() == null */) {
      try {
        resolve(rootArtifact, request.getRemoteRepositories(), session);
      } catch (ArtifactResolutionException e) {
        result.addErrorArtifactException(e);
        return result;
      } catch (ArtifactNotFoundException e) {
        result.addMissingArtifact(request.getArtifact());
        return result;
      }
    }

    ArtifactResolutionRequest collectionRequest = request;

    if (request.isResolveTransitively()) {
      MetadataResolutionRequest metadataRequest = new DefaultMetadataResolutionRequest(request);

      metadataRequest.setArtifact(rootArtifact);
      metadataRequest.setResolveManagedVersions(managedVersions == null);

      try {
        ResolutionGroup resolutionGroup = source.retrieve(metadataRequest);

        if (managedVersions == null) {
          managedVersions = resolutionGroup.getManagedVersions();
        }

        Set<Artifact> directArtifacts = resolutionGroup.getArtifacts();

        if (artifacts == null || artifacts.isEmpty()) {
          artifacts = directArtifacts;
        } else {
          List<Artifact> allArtifacts = new ArrayList<Artifact>();
          allArtifacts.addAll(artifacts);
          allArtifacts.addAll(directArtifacts);

          Map<String, Artifact> mergedArtifacts = new LinkedHashMap<String, Artifact>();
          for (Artifact artifact : allArtifacts) {
            String conflictId = artifact.getDependencyConflictId();
            if (!mergedArtifacts.containsKey(conflictId)) {
              mergedArtifacts.put(conflictId, artifact);
            }
          }

          artifacts = new LinkedHashSet<Artifact>(mergedArtifacts.values());
        }

        collectionRequest = new ArtifactResolutionRequest(request);
        collectionRequest.setServers(request.getServers());
        collectionRequest.setMirrors(request.getMirrors());
        collectionRequest.setProxies(request.getProxies());
        collectionRequest.setRemoteRepositories(resolutionGroup.getResolutionRepositories());
      } catch (ArtifactMetadataRetrievalException e) {
        ArtifactResolutionException are =
            new ArtifactResolutionException(
                "Unable to get dependency information for "
                    + rootArtifact.getId()
                    + ": "
                    + e.getMessage(),
                rootArtifact,
                metadataRequest.getRemoteRepositories(),
                e);
        result.addMetadataResolutionException(are);
        return result;
      }
    }

    if (artifacts == null || artifacts.isEmpty()) {
      if (request.isResolveRoot()) {
        result.addArtifact(rootArtifact);
      }
      return result;
    }

    // After the collection we will have the artifact object in the result but they will not be
    // resolved yet.
    result =
        artifactCollector.collect(
            artifacts,
            rootArtifact,
            managedVersions,
            collectionRequest,
            source,
            collectionFilter,
            listeners,
            null);

    // We have metadata retrieval problems, or there are cycles that have been detected
    // so we give this back to the calling code and let them deal with this information
    // appropriately.

    if (result.hasMetadataResolutionExceptions()
        || result.hasVersionRangeViolations()
        || result.hasCircularDependencyExceptions()) {
      return result;
    }

    if (result.getArtifactResolutionNodes() != null) {
      ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

      CountDownLatch latch = new CountDownLatch(result.getArtifactResolutionNodes().size());

      for (ResolutionNode node : result.getArtifactResolutionNodes()) {
        Artifact artifact = node.getArtifact();

        if (resolutionFilter == null || resolutionFilter.include(artifact)) {
          executor.execute(
              new ResolveTask(
                  classLoader, latch, artifact, session, node.getRemoteRepositories(), result));
        } else {
          latch.countDown();
        }
      }
      try {
        latch.await();
      } catch (InterruptedException e) {
        result.addErrorArtifactException(
            new ArtifactResolutionException("Resolution interrupted", rootArtifact, e));
      }
    }

    // We want to send the root artifact back in the result but we need to do this after the other
    // dependencies
    // have been resolved.
    if (request.isResolveRoot()) {
      // Add the root artifact (as the first artifact to retain logical order of class path!)
      Set<Artifact> allArtifacts = new LinkedHashSet<Artifact>();
      allArtifacts.add(rootArtifact);
      allArtifacts.addAll(result.getArtifacts());
      result.setArtifacts(allArtifacts);
    }

    return result;
  }

  public void resolve(
      Artifact artifact,
      List<ArtifactRepository> remoteRepositories,
      ArtifactRepository localRepository)
      throws ArtifactResolutionException, ArtifactNotFoundException {
    resolve(artifact, remoteRepositories, localRepository, null);
  }

  /** ThreadCreator for creating daemon threads with fixed ThreadGroup-name. */
  static final class DaemonThreadCreator implements ThreadFactory {
    static final String THREADGROUP_NAME =
        "org.apache.maven.artifact.resolver.DefaultArtifactResolver";

    static final ThreadGroup GROUP = new ThreadGroup(THREADGROUP_NAME);

    static final AtomicInteger THREAD_NUMBER = new AtomicInteger(1);

    public Thread newThread(Runnable r) {
      Thread newThread = new Thread(GROUP, r, "resolver-" + THREAD_NUMBER.getAndIncrement());
      newThread.setDaemon(true);
      newThread.setContextClassLoader(null);
      return newThread;
    }
  }

  private class ResolveTask implements Runnable {

    private final ClassLoader classLoader;

    private final CountDownLatch latch;

    private final Artifact artifact;

    private final RepositorySystemSession session;

    private final List<ArtifactRepository> remoteRepositories;

    private final ArtifactResolutionResult result;

    public ResolveTask(
        ClassLoader classLoader,
        CountDownLatch latch,
        Artifact artifact,
        RepositorySystemSession session,
        List<ArtifactRepository> remoteRepositories,
        ArtifactResolutionResult result) {
      this.classLoader = classLoader;
      this.latch = latch;
      this.artifact = artifact;
      this.session = session;
      this.remoteRepositories = remoteRepositories;
      this.result = result;
    }

    public void run() {
      ClassLoader old = Thread.currentThread().getContextClassLoader();
      try {
        Thread.currentThread().setContextClassLoader(classLoader);
        resolve(artifact, remoteRepositories, session);
      } catch (ArtifactNotFoundException anfe) {
        // These are cases where the artifact just isn't present in any of the remote repositories
        // because it wasn't deployed, or it was deployed in the wrong place.

        synchronized (result) {
          result.addMissingArtifact(artifact);
        }
      } catch (ArtifactResolutionException e) {
        // This is really a wagon TransferFailedException so something went wrong after we
        // successfully
        // retrieved the metadata.

        synchronized (result) {
          result.addErrorArtifactException(e);
        }
      } finally {
        latch.countDown();
        Thread.currentThread().setContextClassLoader(old);
      }
    }
  }

  @Override
  public void dispose() {
    if (executor instanceof ExecutorService) {
      ((ExecutorService) executor).shutdownNow();
    }
  }

  public void setWorkspaceCache(MavenWorkspaceCache cache, boolean failOnUnresolvedDependency) {
    workspaceCache = cache;
    this.failOnUnresolvedDependency = failOnUnresolvedDependency;
  }

  public void reset() {
    workspaceCache = null;
  }
}
